HTML <head> 建議寫法
<!-- 提前連線,減少 DNS/SSL 延遲 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 載入字體(以 Noto Sans TC 為例);display=swap 避免字體閃爍 -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;600;800&display=swap" rel="stylesheet">
CSS 字體堆疊(含後援)
body {
font-family: "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, -apple-system, Arial, sans-serif;
}
重點
preconnect
:瀏覽器先建立 TCP/SSL,縮短字體載入時間。
display=swap
:在自訂字體尚未載入時先用系統字體,載入後再替換,避免空白閃爍(FOIT)。
後援字體:確保沒網路或載入失敗時仍可讀。
適合 hover、focus、開闔等 UI 微動效。
.btn {
padding: 10px 16px;
background: #4f46e5;
color: #fff;
border-radius: 10px;
border: none;
cursor: pointer;
/* 只動畫 transform/box-shadow(避免 layout 抖動) */
transition: transform .2s ease, box-shadow .2s ease, background-color .2s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,.12);
background: #4338ca;
}
(B) @keyframes(時間軸驅動的連續動畫)
適合 Loading、進場、注意力聚焦等完整節奏。
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader {
width: 28px; height: 28px; border-radius: 50%;
border: 3px solid #e5e7eb; /* 淺色外圈 */
border-top-color: #4f46e5; /* 亮色進度 */
animation: spin 1s linear infinite; /* 無限旋轉 */
}
<div class="card">卡片內容</div>
.card {
background: #fff;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0,0,0,.06);
transition: transform .18s ease, box-shadow .18s ease;
will-change: transform; /* 提示瀏覽器做圖層加速 */
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 24px rgba(0,0,0,.12);
}
(2) 主要按鈕(Primary Button)
<button class="btn">送出</button>
.btn { /* 見上方 transition 範例 */ }
.btn:active { transform: translateY(-1px) scale(.99); }
(3) 進場淡入(Scroll Reveal)
<section class="reveal">這一段會在滾動到時淡入顯示</section>
.reveal {
opacity: 0;
transform: translateY(12px);
transition: opacity .5s ease, transform .5s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
<script>
// 用 IntersectionObserver 增加 .is-visible
const els = document.querySelectorAll('.reveal');
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('is-visible');
// 若只需要一次,可取消觀察
io.unobserve(e.target);
}
});
}, { threshold: 0.15 }); // 露出 15% 觸發
els.forEach(el => io.observe(el));
</script>
(4) Loading 旋轉(等待 API 或寫入)
<div class="loader" aria-label="載入中" role="status"></div>
/* 見 @keyframes spin 與 .loader 定義 */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
}
(b) 盡量用 transform
與 opacity
避免用 top/left/width/height
做動畫,會觸發 reflow,成本較高。
transform
、opacity
幾乎只需合成層,較順暢。
(c) 針對會動的元素加 will-change
但不要到處亂加(會耗記憶體),僅在常動的元素使用。